/*
 * Copyright (C) 2013 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.picasso;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.net.NetworkInfo;

import com.squareup.picasso.Picasso.Priority;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
import static com.squareup.picasso.Picasso.Priority.LOW;
import static com.squareup.picasso.Utils.OWNER_HUNTER;
import static com.squareup.picasso.Utils.VERB_DECODED;
import static com.squareup.picasso.Utils.VERB_EXECUTING;
import static com.squareup.picasso.Utils.VERB_JOINED;
import static com.squareup.picasso.Utils.VERB_REMOVED;
import static com.squareup.picasso.Utils.VERB_TRANSFORMED;
import static com.squareup.picasso.Utils.getLogIdsForHunter;
import static com.squareup.picasso.Utils.log;

class BitmapHunter implements Runnable {
    /**
     * Global lock for bitmap decoding to ensure that we are only are decoding
     * one at a time. Since this will only ever happen in background threads we
     * help avoid excessive memory thrashing as well as potential OOMs.
     * Shamelessly stolen from Volley.
     */
    private static final Object DECODE_LOCK = new Object();

    private static final ThreadLocal<StringBuilder> NAME_BUILDER = new ThreadLocal<StringBuilder>() {
        @Override
        protected StringBuilder initialValue() {
            return new StringBuilder(Utils.THREAD_PREFIX);
        }
    };

    private static final AtomicInteger SEQUENCE_GENERATOR = new AtomicInteger();

    private static final RequestHandler ERRORING_HANDLER = new RequestHandler() {
        @Override
        public boolean canHandleRequest(Request data) {
            return true;
        }

        @Override
        public Result load(Request data, Cache cache) throws IOException {
            throw new IllegalStateException("Unrecognized type of request: " + data);
        }
    };

    final int sequence;
    final Picasso picasso;
    final Dispatcher dispatcher;
    final Cache cache;
    final Stats stats;
    final String key;
    final Request data;
    final boolean skipMemoryCache;
    final boolean updateMemoryCache;
    final RequestHandler requestHandler;

    Action action;
    List<Action> actions;
    Bitmap result;
    Future<?> future;
    Picasso.LoadedFrom loadedFrom;
    Exception exception;
    int exifRotation; // Determined during decoding of original resource.
    int retryCount;
    Priority priority;

    BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action,
                 RequestHandler requestHandler) {
        this.sequence = SEQUENCE_GENERATOR.incrementAndGet();
        this.picasso = picasso;
        this.dispatcher = dispatcher;
        this.cache = cache;
        this.stats = stats;
        this.key = action.getKey();
        this.data = action.getRequest();
        this.skipMemoryCache = action.skipCache;
        this.updateMemoryCache = action.updateCache;
        this.requestHandler = requestHandler;
        this.retryCount = requestHandler.getRetryCount();
        this.action = action;
        this.priority = (action != null ? action.getPriority() : LOW);
    }

    @Override
    public void run() {
        try {
            updateThreadName(data);

            if (picasso.loggingEnabled) {
                log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
            }

            result = hunt();

            if (result == null) {
                dispatcher.dispatchFailed(this);
            } else {
                dispatcher.dispatchComplete(this);
            }
        } catch (Downloader.ResponseException e) {
            exception = e;
            dispatcher.dispatchFailed(this);
        } catch (IOException e) {
            exception = e;
            dispatcher.dispatchRetry(this);
        } catch (OutOfMemoryError e) {
            StringWriter writer = new StringWriter();
            stats.createSnapshot().dump(new PrintWriter(writer));
            exception = new RuntimeException(writer.toString(), e);
            dispatcher.dispatchFailed(this);
        } catch (Exception e) {
            exception = e;
            dispatcher.dispatchFailed(this);
        } finally {
            Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
        }
    }

    void download() throws IOException {
        if (requestHandler instanceof NetworkRequestHandler) {
            ((NetworkRequestHandler) requestHandler).download(data, cache);
        }
    }

    InputStream getStream() throws IOException {
        if (requestHandler instanceof NetworkRequestHandler) {
           return ((NetworkRequestHandler) requestHandler).getStream(data, cache);
        }
        return null;
    }

    Bitmap hunt() throws IOException {
        Bitmap bitmap = null;
        if (updateMemoryCache) {
            bitmap = cache.get(key);
            if (bitmap != null) {
                bitmap.recycle();
            }
        } else if (!skipMemoryCache) {
            bitmap = cache.get(key);
            if (bitmap != null) {
                stats.dispatchCacheHit();
                loadedFrom = MEMORY;
                if (picasso.loggingEnabled) {
                    log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
                }
                return bitmap;
            }
        }

        data.loadFromLocalCacheOnly = (retryCount == 0);
        RequestHandler.Result result = requestHandler.load(data, cache);
        if (result != null) {
            bitmap = result.getBitmap();
            loadedFrom = result.getLoadedFrom();
            exifRotation = result.getExifOrientation();
        }

        if (bitmap != null) {
            if (picasso.loggingEnabled) {
                log(OWNER_HUNTER, VERB_DECODED, data.logId());
            }
            stats.dispatchBitmapDecoded(bitmap);
            if (data.needsTransformation() || exifRotation != 0) {
                synchronized (DECODE_LOCK) {
                    if (data.needsMatrixTransform() || exifRotation != 0) {
                        bitmap = transformResult(data, bitmap, exifRotation);
                        if (picasso.loggingEnabled) {
                            log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
                        }
                    }
                    if (data.hasCustomTransformations()) {
                        bitmap = applyCustomTransformations(data.transformations, bitmap);
                        if (picasso.loggingEnabled) {
                            log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
                        }
                    }
                }
                if (bitmap != null) {
                    stats.dispatchBitmapTransformed(bitmap);
                }
            }
        }

        return bitmap;
    }

    void attach(Action action) {
        boolean loggingEnabled = picasso.loggingEnabled;
        Request request = action.request;

        if (this.action == null) {
            this.action = action;
            if (loggingEnabled) {
                if (actions == null || actions.isEmpty()) {
                    log(OWNER_HUNTER, VERB_JOINED, request.logId(), "to empty hunter");
                } else {
                    log(OWNER_HUNTER, VERB_JOINED, request.logId(), getLogIdsForHunter(this, "to "));
                }
            }
            return;
        }

        if (actions == null) {
            actions = new ArrayList<Action>(3);
        }

        actions.add(action);

        if (loggingEnabled) {
            log(OWNER_HUNTER, VERB_JOINED, request.logId(), getLogIdsForHunter(this, "to "));
        }

        Priority actionPriority = action.getPriority();
        if (actionPriority.ordinal() > priority.ordinal()) {
            priority = actionPriority;
        }
    }

    void detach(Action action) {
        boolean detached = false;
        if (this.action == action) {
            this.action = null;
            detached = true;
        } else if (actions != null) {
            detached = actions.remove(action);
        }

        // The action being detached had the highest priority. Update this
        // hunter's priority with the remaining actions.
        if (detached && action.getPriority() == priority) {
            priority = computeNewPriority();
        }

        if (picasso.loggingEnabled) {
            log(OWNER_HUNTER, VERB_REMOVED, action.request.logId(), getLogIdsForHunter(this, "from "));
        }
    }

    private Priority computeNewPriority() {
        Priority newPriority = LOW;

        boolean hasMultiple = actions != null && !actions.isEmpty();

        // Hunter has no requests, low priority.
        if (actions == null && !hasMultiple) {
            return newPriority;
        }

        if (action != null) {
            newPriority = action.getPriority();
        }

        if (hasMultiple) {
            for (int i = 0, n = actions.size(); i < n; i++) {
                Priority actionPriority = actions.get(i).getPriority();
                if (actionPriority.ordinal() > newPriority.ordinal()) {
                    newPriority = actionPriority;
                }
            }
        }

        return newPriority;
    }

    boolean cancel() {
        return action == null && (actions == null || actions.isEmpty()) && future != null && future.cancel(false);
    }

    boolean isCancelled() {
        return future != null && future.isCancelled();
    }

    boolean shouldSkipMemoryCache() {
        return skipMemoryCache;
    }

    boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
        boolean hasRetries = retryCount > 0;
        if (!hasRetries) {
            return false;
        }
        retryCount--;
        return requestHandler.shouldRetry(airplaneMode, info);
    }

    boolean supportsReplay() {
        return requestHandler.supportsReplay();
    }

    Bitmap getResult() {
        return result;
    }

    String getKey() {
        return key;
    }

    Request getRequest() {
        return data;
    }

    Action getAction() {
        return action;
    }

    Picasso getPicasso() {
        return picasso;
    }

    List<Action> getActions() {
        return actions;
    }

    Exception getException() {
        return exception;
    }

    Picasso.LoadedFrom getLoadedFrom() {
        return loadedFrom;
    }

    Priority getPriority() {
        return priority;
    }

    static void updateThreadName(Request data) {
        String name = data.getName();
        StringBuilder builder = NAME_BUILDER.get();
        builder.ensureCapacity(Utils.THREAD_PREFIX.length() + name.length());
        builder.replace(Utils.THREAD_PREFIX.length(), builder.length(), name);
        Thread.currentThread().setName(builder.toString());
    }

    static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) {
        Request request = action.getRequest();
        List<RequestHandler> requestHandlers = picasso.getRequestHandlers();

        // Index-based loop to avoid allocating an iterator.
        // noinspection ForLoopReplaceableByForEach
        for (int i = 0, count = requestHandlers.size(); i < count; i++) {
            RequestHandler requestHandler = requestHandlers.get(i);
            if (requestHandler.canHandleRequest(request)) {
                return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler);
            }
        }

        return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
    }

    static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) {
        for (int i = 0, count = transformations.size(); i < count; i++) {
            final Transformation transformation = transformations.get(i);
            Bitmap newResult = transformation.transform(result);

            if (newResult == null) {
                final StringBuilder builder = new StringBuilder()
                        //
                        .append("Transformation ").append(transformation.key()).append(" returned null after ")
                        .append(i).append(" previous transformation(s).\n\nTransformation list:\n");
                for (Transformation t : transformations) {
                    builder.append(t.key()).append('\n');
                }
                Picasso.HANDLER.post(new Runnable() {
                    @Override
                    public void run() {
                        throw new NullPointerException(builder.toString());
                    }
                });
                return null;
            }

            if (newResult == result && result.isRecycled()) {
                Picasso.HANDLER.post(new Runnable() {
                    @Override
                    public void run() {
                        throw new IllegalStateException("Transformation " + transformation.key()
                                + " returned input Bitmap but recycled it.");
                    }
                });
                return null;
            }

            // If the transformation returned a new bitmap ensure they recycled
            // the original.
            if (newResult != result && !result.isRecycled()) {
                Picasso.HANDLER.post(new Runnable() {
                    @Override
                    public void run() {
                        throw new IllegalStateException("Transformation " + transformation.key()
                                + " mutated input Bitmap but failed to recycle the original.");
                    }
                });
                return null;
            }

            result = newResult;
        }
        return result;
    }

    /**
     * 图片旋转、缩放
     *
     * @param data
     * @param result
     * @param exifRotation
     * @return
     */
    static Bitmap transformResult(Request data, Bitmap result, int exifRotation) {
        int inWidth = result.getWidth();
        int inHeight = result.getHeight();

        int drawX = 0;
        int drawY = 0;
        int drawWidth = inWidth;
        int drawHeight = inHeight;

        Matrix matrix = new Matrix();

        if (data.needsMatrixTransform()) {
            int targetWidth = data.targetWidth;
            int targetHeight = data.targetHeight;

            float targetRotation = data.rotationDegrees;
            if (targetRotation != 0) {
                if (data.hasRotationPivot) {
                    matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY);
                } else {
                    matrix.setRotate(targetRotation);
                }
            }

            if (!data.targetSizeAsMax) {
                if (data.centerCrop) {
                    float widthRatio = targetWidth / (float) inWidth;
                    float heightRatio = targetHeight / (float) inHeight;
                    float scale;
                    if (widthRatio > heightRatio) {
                        scale = widthRatio;
                        int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio));
                        drawY = (inHeight - newSize) / 2;
                        drawHeight = newSize;
                    } else {
                        scale = heightRatio;
                        int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio));
                        drawX = (inWidth - newSize) / 2;
                        drawWidth = newSize;
                    }
                    matrix.preScale(scale, scale);
                } else if (data.centerInside) {
                    float widthRatio = targetWidth / (float) inWidth;
                    float heightRatio = targetHeight / (float) inHeight;
                    float scale = widthRatio < heightRatio ? widthRatio : heightRatio;
                    matrix.preScale(scale, scale);
                } else if (targetWidth != 0 && targetHeight != 0 //
                        && (targetWidth != inWidth || targetHeight != inHeight)) {
                    // If an explicit target size has been specified and they do not
                    // match the results bounds,
                    // pre-scale the existing matrix appropriately.
                    float sx = targetWidth / (float) inWidth;
                    float sy = targetHeight / (float) inHeight;
                    matrix.preScale(sx, sy);
                }
            }

        }

        if (exifRotation != 0) {
            matrix.preRotate(exifRotation);
        }

        Bitmap newResult = Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true);
        if (newResult != result) {
            result.recycle();
            result = newResult;
        }

        return result;
    }
}
